查看原文
其他

主题配置繁琐?Compose帮你轻松搞定!

RugerMc Jetpack Compose 博物馆 2022-07-13

初识 MaterialTheme

MaterialTheme  是 Jetpack Compose 所提供的基于 Material Design 风格主题样式模版,通过主题样式模版的配置,允许自定义视图系统中所有组件根据主题切换而相应得到样式改变。

当创建一个新的 Compose 项目时,Android Studio 会默认帮我生成一个 Theme 方法(生成的命名规则:项目名称+Theme)

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // 看这里,我创建的项目名称是 ComposeStudy ~
            // 值得注意的是我们声明的自定义视图会以 lambda 参数形式传入其中。
            ComposeStudyTheme {  
               Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

接下来,我们看看这个生成的 Theme 方法为我们做了哪些事。

@Composable
fun ComposeStudyTheme(
    darkTheme: Boolean = isSystemInDarkTheme()
,
    content: @Composable() () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }
    MaterialTheme(
        colors = colors, // 颜色
        typography = Typography, // 字体
        shapes = Shapes, // 形状
        content = content // 声明的视图
    )
}
private val DarkColorPalette = darkColors(
    primary = Purple200,
    primaryVariant = Purple700,
    secondary = Teal200
)
private val LightColorPalette = lightColors(
    primary = Purple500,
    primaryVariant = Purple700,
    secondary = Teal200
)

在这里我们看到了 MaterialTheme 。但是先别急我们往上看看,可以看到 Android Studio 默认帮助我们生成了两种配色的调色板( Light 与 Dark ),根据传入布尔值的不同而选择其一,并将其传入到MaterialTheme。可以看到这两种配色的调色板分别使用的是 darkColorslightColors 两个方法的返回值,我们看看这两者的实现。

fun lightColors(
    primary: Color = Color(0xFF6200EE)
,
    primaryVariant: Color = Color(0xFF3700B3),
    secondary: Color = Color(0xFF03DAC6),
    secondaryVariant: Color = Color(0xFF018786),
    background: Color = Color.White,
    surface: Color = Color.White,
    error: Color = Color(0xFFB00020),
    onPrimary: Color = Color.White,
    onSecondary: Color = Color.Black,
    onBackground: Color = Color.Black,
    onSurface: Color = Color.Black,
    onError: Color = Color.White
): Colors = Colors(
    primary,
    primaryVariant,
    secondary,
    secondaryVariant,
    background,
    surface,
    error,
    onPrimary,
    onSecondary,
    onBackground,
    onSurface,
    onError,
    true
)

可以看到 lightColors 将传入参数透传到了Colors构造器中了,而 Colors 构造器属性是没有默认值的,lightColors 帮助我们生成了许多属性默认值。可以发现两种调色板本质上只是Colors成员属性配置的不同,懂得了本质就可以进行定制主题样式的配置了。

简单使用 MaterialTheme

接下来是使用示例,假设当前需求为根据主题的不同使得我们定制的文本颜色也会随之变化。当亮色主题时显示为红色,暗色主题显示为蓝色。这里我们使用 Color 的 primary 属性来存储,当然也可以使用其他成员属性。

@Composable
fun CustomColorTheme(
    isDark: Boolean,
    content: @Composable() () -> Unit
)
 {
    var BLUE = Color(0xFF0000FF
    var RED = Color(0xFFDC143C)
    val colors = if (isDark) {
        darkColors(primary = BLUE) // 将 primary 设置为蓝色
    } else {
        lightColors(primary = RED) // 将 primary 设置为红色
    }
    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

配置完就可以在我们的自定义视图系统中使用了,将我们视图中的 Text 颜色配置为 MaterialTheme.colors.primary

@Composable
fun SampleText() {
    Text(
        text = "Hello World",
        color = MaterialTheme.colors.primary
    )
}
@Preview(showBackground = true)
@Composable
fun DarkPreview() {
    CustomColorTheme(isDark = true) {
        SampleText();
    }
}
@Preview(showBackground = true)
@Composable
fun LightPreview() {
    CustomColorTheme(isDark = false) {
        SampleText()
    }
}

我们同时创建了两种主题的预览,通过 Android Studio 的 Preview 窗口就可以预览到所有主题下的效果了。


MaterialTheme 是怎么做到的

为深入理解 MaterialTheme 工作原理,我们需要进入源码一探究竟。

需要注意的是,此时传入的 content 参数其实是声明在 Theme 中的自定义布局系统,其类型是一个带有 Composable 注解的 lambda (对于这类带有 Composable 的 lambda 简称为 composable )。

我们所关注的 colors 被 remember 修饰后赋值为 rememberedColors。如果 MaterialTheme 这个 composable 发生 recompose 时便会检查 colors 是否发生了改变从而决定更新。

接下来使用 CompositionLocalProvider 方法,通过中缀 providers 将 rememberedColors 提供给了 LocalColors。让我们回到自定义视图中,看看我们是如何通过 MaterialTheme 获取到当前主题配色的。

object MaterialTheme {
    val colors: Colors
        @Composable
        @ReadOnlyComposable
        get() = LocalColors.current
    val typography: Typography
        @Composable
        @ReadOnlyComposable
        get() = LocalTypography.current
    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = LocalShapes.current
}

可以发现在获取到当前主题配色时使用的是 MaterialTheme 类单例的 colors 属性,间接使用了 LocalColors。

总结来说,我们在自定义 Theme 使用的是 MaterialTheme 函数为 LocalColors 赋值,而在获取时使用的是 MaterialTheme 类单例,间接从 LocalColors 中获取到值。所以 LocalColors 到底是何方神圣呢?

internal val LocalColors = staticCompositionLocalOf { lightColors() }

通过声明可以发现他实际上是一个 CompositionLocal,其初始值是 lightColors() 返回的 colors 配置。

MaterialTheme 方法中通过 CompositionLocalProvider 方法为我们的自定义视图 composable 提供了一些 CompositionLocal,包含了所有的主题配置信息。

CompositionLocal 介绍

很多时候我们需要在 composable 树中共享一些数据(例如主题配置),一种有效方式就是通过显式参数传递的方式进行实现,当参数越来越多时,composable 参数列表会变得越来越臃肿,难以进行维护。当 composable 需要彼此间传递数据,并且实现各自的私有性时,如果仍采用显式参数传递的方式则可能会产生意料之外的麻烦与崩溃。

为解决上述痛点问题, Jetpack Compose 提供了 CompostionLocal 用来完成 composable 树中共享数据方式。CompositionLocals 是具有层级的,可以被限定在以某个 composable 作为根结点的子树中,其默认会向下传递的,当然当前子树中的某个 composable 可以对该 CompositionLocals 进行覆盖,从而使得新值会在这个 composable 中继续向下传递。

Jetpack Compose 为我们提供了compositionLocalOf 方法用于创建一个 CompostionLocal 实例。

import androidx.compose.runtime.compositionLocalOf

var LocalString = compositionLocalOf { "Jetpack Compose" }

在 composable 树的某个地方,我们可以使用 CompositionLocalProvider 方法为 CompositionLocal 提供一个值。通常情况下位于 composable 树的根部,但也可以位于任何位置,还可以在多个位置使用,以覆盖子树能够获取到的值。我们的示例选择在 Column 所包含的 composable 中使用 CompositionLocalProvider。

import androidx.compose.runtime.CompositionLocalProvider

setContent {
    CustomColorTheme(true) {
        Column {
            CompositionLocalProvider(
                LocalString provides "Hello World"
            ) {
                Text(
                    text = LocalString.current,
                    color = Color.Green
                )
                CompositionLocalProvider(
                    LocalString provides "Ruger McCarthy"
                ) {
                    Text(
                        text = LocalString.current,
                        color = Color.Blue
                    )
                }
            }
            Text(
                text = LocalString.current,
                color = Color.Red
            )
        }
    }
}

实际效果可以看到,虽然所有 composable 均依赖的是同一个 CompositionLocal,而其获得到的实际的值却是不一样的。


自定义你的主题方案

本文示例来自:https://github.com/RugerMcCarthy/BloomCompose

通过阅读前两篇文章相信你已经具备自定义主题方案的能力了。我们通过#AndroidDevChallange挑战赛第三周题目作为示例来看看在实际项目中如何进行应用。在不同主题方案下背景颜色、文字颜色与图片资源都是不同的。值得注意的是对于所有文本也可以通过主题完成字体样式的配置,所要实现的目标效果如下图所示。


配置颜色样式

首先,我们来学习如何配置颜色样式。其实这里的内容在初识MaterialTheme章节中的操作是一样的。我们仅需要根据主题的不同生成其对应的colors即可。根据项目需求,我们进行以下的配置。

private val BloomLightColorPaltte = lightColors(
    primary = pink100,
    secondary = pink900,
    background = white,
    surface = white850,
    onPrimary = gray,
    onSecondary = white,
    onBackground = gray,
    onSurface = gray,
)

private val BloomDarkColorPaltte = darkColors(
    primary = green900,
    secondary = green300,
    background = gray,
    surface = white150,
    onPrimary = white,
    onSecondary = gray,
    onBackground = white,
    onSurface = white850
)

@Composable
fun BloomTheme(theme: BloomTheme = BloomTheme.LIGHT, content: @Composable() () -> Unit) {
    CompositionLocalProvider(
        LocalWelcomeAssets provides if (theme == BloomTheme.DARK) WelcomeAssets.DarkWelcomeAssets else WelcomeAssets.LightWelcomeAssets,
    ) {
        MaterialTheme(
            colors = if (theme == BloomTheme.DARK) BloomDarkColorPaltte else BloomLightColorPaltte,
            typography = Typography,
            shapes = shapes,
            content = content
        )
    }
}

在我们的视图所需要Color的地方配置即可。

Text(
    text = "Beautiful home garden solutions",
    textAlign = TextAlign.Center,
    color = MaterialTheme.colors.onPrimary // I'm here
)

配置字体样式

我们接着来学习如何配置字体样式。还记得MaterialTheme方法嘛,其实第二个参数typography表示的就是你所配置的字体,只是这个Typography是Android Studio默认帮你配制的。

MaterialTheme(
   colors = colors,
   typography = Typography,
   shapes = Shapes,
   content = content
)

如果是新建的项目,Android Studio会在ui.theme包下生成Type.kt,其中包含了Typography的实现,名为Typography的变量间接调用Typography类构造函数。

val Typography = Typography(
    body1 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    )
)

再回到MaterialTheme实现,可以发现typography提供给LocalTypography这个CompositionLocal实例了,那么我们在项目中如何使用这个特殊字体也不需要额外的介绍了,这与colors是完全一样的。

@Composable
fun MaterialTheme(
    colors: Colors = MaterialTheme.colors,
    typography: Typography = MaterialTheme.typography,
    shapes: Shapes = MaterialTheme.shapes,
    content: @Composable () -> Unit
)
 {
    val rememberedColors = remember {
        colors.copy()
    }.apply { updateColorsFrom(colors) }
    val rippleIndication = rememberRipple()
    val selectionColors = rememberTextSelectionColors(rememberedColors)
    CompositionLocalProvider(
        LocalColors provides rememberedColors,
        LocalContentAlpha provides ContentAlpha.high,
        LocalIndication provides rippleIndication,
        LocalRippleTheme provides MaterialRippleTheme,
        LocalShapes provides shapes,
        LocalTextSelectionColors provides selectionColors,
        LocalTypography provides typography // I'm here~
    ) {
        ProvideTextStyle(value = typography.body1, content = content)
    }
}

既然懂得了原理,我们仅需要根据项目实际需求配置字体样式即可,既然Android Studio帮助生成Type.kt,说明是官方希望我们将字体样式配置在这个文件中的。这是一种规范,但也可不遵守。

值得注意的是由于每种字体都会有不同的粗细风格,我们在字体样式配置时需要指明字体种类与粗细风格。

val nunitoSansFamily = FontFamily(
    Font(R.font.nunitosans_light, FontWeight.Light),
    Font(R.font.nunitosans_semibold, FontWeight.SemiBold),
    Font(R.font.nunitosans_bold, FontWeight.Bold)
)
val bloomTypography = Typography(
    h1 = TextStyle(
        fontSize = 18.sp,
        fontFamily = nunitoSansFamily,
        fontWeight = FontWeight.Bold
    ),
    h2 = TextStyle(
        fontSize = 14.sp,
        letterSpacing = 0.15.sp,
        fontFamily = nunitoSansFamily,
        fontWeight = FontWeight.Bold
    ),
    ....
)

使用的话就很简单了,我们仅需将字体样式传入MaterialTheme即可。

@Composable
fun BloomTheme(theme: BloomTheme = BloomTheme.LIGHT, content: @Composable() () -> Unit) {
    MaterialTheme(
         colors = if (theme == BloomTheme.DARK) BloomDarkColorPaltte else BloomLightColorPaltte,
         typography = bloomTypoGraphy,
         shapes = shapes,
         content = content
    )
}

在我们的视图组件中使用style参数进行配置即可。

Text(
    text = "Beautiful home garden solutions",
    textAlign = TextAlign.Center,
    style = MaterialTheme.typography.subtitle1, // I'm here
    color = MaterialTheme.colors.onPrimary
)

配置自定义资源

有时我们可能需要根据主题的不同使用不同的多媒体资源,例如图片、视频、音频等等。通过查阅MaterialTheme参数列表我们没有发现可以进行配置的参数。难道 Jetpack Compose 不具备这样的能力?答案当然是否定的,Android团队已经充分考虑了各种场景,只是针对于这种需求而言,我们需要进行额外的定制扩展。

在前一篇文章中,我们已经详细介绍了MaterialTheme工作原理,想必你也猜到了,就是通过定制CompositionLocal方式来实现图片资源的扩展,根据主题的不同选用其对应的多媒体资源。

open class WelcomeAssets private constructor(
    var background: Int,
    var illos: Int,
    var logo: Int
) {
    object LightWelcomeAssets : WelcomeAssets(
        background = R.drawable.ic_light_welcome_bg,
        illos = R.drawable.ic_light_welcome_illos,
        logo = R.drawable.ic_light_logo
    )

    object DarkWelcomeAssets : WelcomeAssets(
        background = R.drawable.ic_dark_welcome_bg,
        illos = R.drawable.ic_dark_welcome_illos,
        logo = R.drawable.ic_dark_logo
    )
}

internal var LocalWelcomeAssets = staticCompositionLocalOf {
   WelcomeAssets.LightWelcomeAssets as WelcomeAssets
}

于此同时,我们还希望能够在视图中仍通过MaterialTheme来访问我们的图片资源,那么则可以通过Kotlin扩展属性的特性进行实现(扩展属性是没有幕后字段的,只能委托其他实例)。值得注意的是,CompositionLocal只能在composable(带有Composable注解的lambda)中使用,所以我们需要为这个属性获取添加@Composable与@ReadOnlyComposable注解。

val MaterialTheme.welcomeAssets
    @Composable
    @ReadOnlyComposable
    get() = LocalWelcomeAssets.current

这样我们在视图中就可以仍然通过MaterialTheme来获取扩展的图片资源了。

Image(
     painter = rememberVectorPainter(image = ImageVector.vectorResource(id = MaterialTheme.welcomeAssets.background)),
     contentDescription = "weclome_bg",
     modifier = Modifier.fillMaxSize()
)

既然了解了图片的主题配置,其他多媒体资源的主题配置是完全相同的。


推荐阅读

Compose 1.0 即将发布,你准备好了吗?

使用Jetpack Compose完成你的自定义Layout


Compose 博物馆网站:https://compose.net.cn/


加作者微信,拉你进 Compose 技术交流群


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存